// Copyright 2021-present The Atlas Authors. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.

package dm

import (
	"fmt"
	"math"
	"strconv"
	"strings"

	"gitee.com/damengde/atlas/sql/internal/sqlx"
	"gitee.com/damengde/atlas/sql/schema"
)

// FormatType converts schema type to its column form in the database.
// An error is returned if the type cannot be recognized.
func FormatType(t schema.Type) (string, error) {
	var f string
	switch t := t.(type) {
	case *BitType:
		f = strings.ToUpper(t.T)
		if t.Size > 1 {
			// The default size is 1. Thus, both
			// BIT and BIT(1) are formatted as bit.
			f += fmt.Sprintf("(%d)", t.Size)
		}
	case *schema.BoolType:
		// Map all flavors to a single form.

	case *schema.BinaryType:
		f = strings.ToUpper(t.T)
		// Accept 0 as a valid size, and avoid appending the default size of type BINARY.
	case *schema.DecimalType:
		if f = strings.ToLower(t.T); f != TypeDecimal && f != TypeNumeric {
			return "", fmt.Errorf("unexpected decimal type: %q", t.T)
		}
		switch p, s := t.Precision, t.Scale; {
		case p < 0 || s < 0:
			return "", fmt.Errorf("decimal type must have precision > 0 and scale >= 0: %d, %d", p, s)
		case p < s:
			return "", fmt.Errorf("decimal type must have precision >= scale: %d < %d", p, s)
		case p == 0 && s == 0:
			// The default value for precision is 10 (i.e. decimal(0,0) = decimal(10)).
			p = 10
			fallthrough
		case s == 0:
			// In standard SQL, the syntax DECIMAL(M) is equivalent to DECIMAL(M,0),
			f = fmt.Sprintf("decimal(%d)", p)
		default:
			f = fmt.Sprintf("decimal(%d,%d)", p, s)
		}
		if t.Unsigned {
			f += " unsigned"
		}
	case *schema.EnumType:
		f = fmt.Sprintf("enum(%s)", formatValues(t.Values))
	case *schema.FloatType:
		f = strings.ToLower(t.T)
		// FLOAT with precision > 24, become DOUBLE.
		// Also, REAL is a synonym for DOUBLE (if REAL_AS_FLOAT was not set).
		if f == TypeFloat && t.Precision > 24 || f == TypeReal {
			f = TypeDouble
		}
		if t.Unsigned {
			f += " unsigned"
		}
	case *schema.IntegerType:
		f = strings.ToLower(t.T)
		if t.Unsigned {
			f += " unsigned"
		}
	case *schema.JSONType:
		f = strings.ToLower(t.T)
	case *SetType:
		f = fmt.Sprintf("set(%s)", formatValues(t.Values))
	case *schema.StringType:
		f = strings.ToLower(t.T)
		switch f {
		case TypeChar:
			// Not a single char.
			if t.Size > 0 {
				f += fmt.Sprintf("(%d)", t.Size)
			}
		case TypeVarchar:
			// Zero is also a valid length.
			f = fmt.Sprintf("varchar(%d)", t.Size)
		}
	case *schema.SpatialType:
		f = strings.ToLower(t.T)
	case *schema.TimeType:
		f = strings.ToLower(t.T)
		if p := t.Precision; p != nil && *p > 0 {
			f = fmt.Sprintf("%s(%d)", f, *p)
		}
	case *schema.UUIDType:
		f = strings.ToLower(t.T)
	case *schema.UnsupportedType:
		// Do not accept unsupported types as we should cover all cases.
		return "", fmt.Errorf("unsupported type %q", t.T)
	default:
		return "", fmt.Errorf("invalid schema type %T", t)
	}
	return f, nil
}
func Parse(raw string) (schema.Type, error) {
	opts := strings.Split(raw, "|")
	if len(opts) < 1 {
		return ParseType(opts[0], 0, 0, 0)
	}
	size, err := strconv.Atoi(opts[1])
	if err != nil {
		return nil, fmt.Errorf("invalid size %q: %w", opts[1], err)
	}
	numericPrecision, err := strconv.Atoi(opts[2])
	if err != nil {
		return nil, fmt.Errorf("invalid size %q: %w", opts[2], err)
	}
	numericScale, err := strconv.Atoi(opts[3])
	if err != nil {
		return nil, fmt.Errorf("invalid size %q: %w", opts[3], err)
	}
	return ParseType(opts[0], size, numericPrecision, numericScale)
}

// ParseType returns the schema.Type value represented by the given raw type.
// The raw value is expected to follow the format in MySQL information schema.
func ParseType(raw string, size, numericPrecision, numericScale int) (schema.Type, error) {
	switch raw {
	case TypeBit:
		return &BitType{
			T:    raw,
			Size: size,
		}, nil
	case TypeChar, TypeCharacter, TypeVarchar:
		return &schema.StringType{
			T:    raw,
			Size: size,
		}, nil

	case TypeNumeric, TypeNumber, TypeDecimal, TypeDec:
		return &schema.DecimalType{
			T:         raw,
			Precision: numericPrecision,
			Scale:     numericScale,
		}, nil

	case TypeInteger, TypeBigint, TypeInt, TypeSmallint:
		return &schema.IntegerType{
			T: raw,
			Attrs: []schema.Attr{
				&DisplayWidth{
					N: size,
				}}}, nil
	case TypeTinyint, TypeByte:
		if size == 1 {
			return &schema.BoolType{
				T: raw,
			}, nil
		}
		return &schema.IntegerType{
			T: raw,
			Attrs: []schema.Attr{
				&DisplayWidth{
					N: size,
				}}}, nil
	case TypeBinary, TypeVarbinary:
		return &schema.BinaryType{T: raw, Size: &size}, nil
	case TypeReal, TypeFloat, TypeDouble, TypeDoublePrecision:
		return &schema.FloatType{
			T:         raw,
			Precision: numericPrecision,
		}, nil
	case TypeDate, TypeTime, TypeTimestamp, TypeDatetime:
		return &schema.TimeType{
			T:         raw,
			Precision: &numericPrecision,
			Scale:     &numericScale,
			Attrs: []schema.Attr{&DisplayWidth{
				N: size,
			}},
		}, nil
	case TypeText, TypeLongvarchar:
		return &schema.StringType{
			T:    raw,
			Size: math.MaxUint32,
		}, nil

	case TypeImage, TypeLongvarbinary, TypeBfile:
		maxUint32 := math.MaxUint32
		return &schema.BinaryType{T: raw, Size: &maxUint32}, nil

	case TypeBlob, TypeClob:
		return &schema.BinaryType{T: raw, Size: &size}, nil
	default:
		return nil, fmt.Errorf("unknown column type %q", raw)
	}
}

// formatValues formats ENUM and SET values.
func formatValues(vs []string) string {
	values := make([]string, len(vs))
	for i := range vs {
		values[i] = vs[i]
		if !sqlx.IsQuoted(values[i], '"', '\'') {
			values[i] = "'" + values[i] + "'"
		}
	}
	return strings.Join(values, ",")
}
